package cn.ratel.ioc;

import cn.ratel.aop.ProxyKit;
import cn.ratel.component.*;
import cn.ratel.ioc.annotation.*;
import cn.ratel.kit.CollectionKit;
import cn.ratel.kit.PropKit;
import cn.ratel.kit.StrKit;
import cn.ratel.route.ActionHandler;
import cn.ratel.route.DefaultHandlers;
import cn.ratel.tool.ClassSearcher;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class IocKit {
	private static Logger log = LoggerFactory.getLogger(IocKit.class);
	private static Map<String, Object> beanMap;

	private IocKit() {
	}

	public static <T> T getBean(Class<?> clazz) {
		return (T) beanMap.get(clazz.getName());
	}

	public static <T> T getBean(String className) {
		return (T) beanMap.get(className);
	}

	public static void init() {
		// build-in component init and config
		Configuration configuration = initConfiguration();

		// collect beans to be injected
		Set<Class<?>> classes = ClassSearcher.scan(Constants.me().getScanPackage(), IocKit::isIocBeanClass);
		Preconditions.checkArgument(CollectionKit.isNotEmpty(classes), "start ioc plugin error : can not find any bean class");
		Map<String, Object> beanMap = new HashMap<>();
		for (Class<?> clazz : classes) {
			String beanName = clazz.getName();
			Object enhanceBean = ProxyKit.enhance(clazz);
			beanMap.put(beanName, enhanceBean);
		}
		IocKit.beanMap = beanMap;

		// inject
		inject();

		configuration.afterStart();
	}

	private static Configuration initConfiguration() {
		PropKit.use("config.txt");
		String configClassName = PropKit.get("config.class");
		Class<?> configClass;
		if (StrKit.notBlank(configClassName)) {
			try {
				configClass = Class.forName(configClassName);
			} catch (ClassNotFoundException e) {
				throw new RuntimeException(e);
			}
		} else {
			Set<Class<?>> classes = ClassSearcher.scan("", Configuration.class::isAssignableFrom);
			Preconditions.checkArgument(classes != null, "can not find Configuration class, please add [config.class] to config.txt");
			Preconditions.checkArgument(classes.size() == 1, "Configuration class must be only one");
			configClass = Lists.newArrayList(classes).get(0);
		}
		return initConfiguration(configClass);
	}

	private static Configuration initConfiguration(Class<?> beanClass) {
		Configuration configuration;
		try {
			configuration = (Configuration) beanClass.newInstance();
		} catch (InstantiationException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}

		configuration.configConstants(Constants.me());
		PropKit.use("config-" + Constants.me().getProfile() + ".txt");

		configuration.beforeStart();

		Handlers handlers = Handlers.me();
		for (Handlers.Handler defaultHandler : DefaultHandlers.values()) {
			handlers.addHandler(defaultHandler);
		}
		configuration.configHandlers(handlers);

		Enhancers enhancers = Enhancers.me();
		configuration.configEnhancers(enhancers);

		Plugins plugins = Plugins.me();
		configuration.configPlugins(plugins);
		Plugins.me().getPlugins().forEach(Plugins.Plugin::start);

		log.debug("====[  handlers  ] = {}", StringUtils.join(handlers.getHandlers(), " , "));
		log.debug("====[interceptors] = {}", StringUtils.join(enhancers.getGlobalInterceptors(), " , "));
		log.debug("====[ enhancers  ] = {}", StringUtils.join(enhancers.getGlobalEnhancers(), " , "));
		log.debug("====[  plugins   ] = {}", StringUtils.join(plugins.getPlugins(), " , "));
		return configuration;
	}

	private static boolean isIocBeanClass(Class<?> clazz) {
		return clazz.isAnnotationPresent(Component.class)
				|| clazz.isAnnotationPresent(Service.class)
				|| clazz.isAnnotationPresent(Controller.class)
				|| clazz.isAnnotationPresent(Model.class);
	}

	private static void inject() {
		for (Object bean : beanMap.values()) {
			Class<?> beanClass = bean.getClass().getSuperclass();
			Field[] fields = beanClass.getDeclaredFields();
			if (fields == null) {
				continue;
			}
			for (Field field : fields) {
				if (field.isAnnotationPresent(Inject.class)) {
					int modifiers = field.getModifiers();
					Preconditions.checkArgument(!Modifier.isFinal(modifiers), "field (%s) to be injected can not be final", field.toString());
					Preconditions.checkArgument(!Modifier.isStatic(modifiers), "field (%s) to be injected can not be static", field.toString());
					String fieldClassName = field.getType().getName();
					Object fieldBean = beanMap.get(fieldClassName);
					Preconditions.checkNotNull(fieldBean, "inject field (%s) error, bean of field not found");
					if (!field.isAccessible()) {
						field.setAccessible(true);
					}
					try {
						field.set(bean, fieldBean);
					} catch (IllegalAccessException e) {
						throw new RuntimeException(e.getMessage(), e);
					}
				}
			}

			Controller controller = beanClass.getAnnotation(Controller.class);
			if (controller != null) {
				initController(beanClass, controller);
			}
		}
		Handlers.me().addHandler(new ActionHandler());
	}

	private static void initController(Class<?> beanClass, Controller controller) {
		String controllerName = controller.value();
		if (StrKit.isBlank(controllerName)) {
			controllerName = StrKit.firstCharToLowerCase(beanClass.getSimpleName().replace("Controller", ""));
		}
		Routes.me().addController(controllerName, beanClass);
	}

	public static void destroy() {
		beanMap.clear();
	}
}
